/*
 * Copyright (c) 2009. Silenus Consultoria, S.L.
 */
package es.silenus.detecta.dao.impl;

import es.silenus.detecta.dao.Page;
import es.silenus.detecta.dao.ReadDAO;

import org.hibernate.Session;
import org.hibernate.SessionFactory;

import org.hibernate.criterion.DetachedCriteria;
import org.hibernate.criterion.Restrictions;

import org.springframework.jdbc.core.simple.SimpleJdbcTemplate;

import org.springframework.orm.hibernate3.support.HibernateDaoSupport;

import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import java.io.Serializable;

import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;

import java.util.Collection;
import java.util.List;
import java.util.Map;

import javax.sql.DataSource;


/**
 * Read only DAO implementation.
 *
 * @author <a href="mailto:malonso@silenus-consultoria.es">Mariano Alonso</a>
 *
 * @param <T> the element.
 * @param <K> the key.
 *
 * @since 25-mar-2009 11:02:42
 */
public class ReadDAOImpl<T, K> extends HibernateDaoSupport implements ReadDAO<T, K> {
	/**
	 * The domain class.
	 */
	protected Class<T> domainClass;

	/**
	 * The entity name.
	 */
	protected String entityName;

	/**
	 * The identifier name.
	 */
	protected String identifierName;

	/**
	 * The data source.
	 */
	protected DataSource dataSource;

	/**
	 * The jdbc template;
	 */
	protected SimpleJdbcTemplate jdbcTemplate;

/**
   * Public constructor.
   */
	public ReadDAOImpl() {
	}

	/**
	 * Retrieves the domain class.
	 *
	 * @return the domain class.
	 */
	@SuppressWarnings("unchecked")
	private Class<T> getDomainClass() {
		Type theGenericClass = getClass().getGenericSuperclass();

		if(theGenericClass instanceof ParameterizedType) {
			ParameterizedType thisType = (ParameterizedType)theGenericClass;

			return (Class<T>)thisType.getActualTypeArguments()[0];
		}

		return null;
	}

	/**
	 * Concrete subclasses can override this for custom initialization behavior. Gets called after population of
	 * this instance's bean properties.
	 *
	 * @throws Exception if DAO initialization fails (will be rethrown as a BeanInitializationException)
	 *
	 * @see org.springframework.beans.factory.BeanInitializationException
	 */
	protected void initDao() throws Exception {
		if(domainClass == null) {
			domainClass = getDomainClass();
		}

		if(identifierName == null) {
			if(entityName != null) {
				identifierName = getSessionFactory().getClassMetadata(entityName).getIdentifierPropertyName();
			} else {
				identifierName = getSessionFactory().getClassMetadata(domainClass).getIdentifierPropertyName();
			}
		}
	}

	/**
	 * Sets the domain class.
	 *
	 * @param domainClass the domain class.
	 */
	public void setDomainClass(final Class<T> domainClass) {
		this.domainClass = domainClass;
	}

	/**
	 * Sets the identifier name.
	 *
	 * @param identifierName the identifier name.
	 */
	public void setIdentifierName(final String identifierName) {
		this.identifierName = identifierName;
	}

	/**
	 * Sets the entity name.
	 *
	 * @param entityName the entity name.
	 */
	public void setEntityName(final String entityName) {
		this.entityName = entityName;
	}

	/**
	 * Sets the data source.
	 *
	 * @param dataSource the data source.
	 */
	public void setDataSource(DataSource dataSource) {
		this.dataSource = dataSource;

		if(dataSource == null) {
			this.jdbcTemplate = null;
		} else {
			this.jdbcTemplate = new SimpleJdbcTemplate(this.dataSource);
		}
	}

	/**
	 * Override this method to provide a find by example list criteria.
	 *
	 * @param criteria the criteria.
	 * @param object the sample object.
	 *
	 * @return the criteria.
	 */
	protected DetachedCriteria prepareListCriteria(final DetachedCriteria criteria, final T object) {
		return criteria;
	}

	/**
	 * Override this method to provide a find by example list criteria.
	 *
	 * @param criteria the criteria.
	 * @param object the sample object.
	 *
	 * @return the criteria.
	 */
	protected DetachedCriteria prepareListCriteria(final DetachedCriteria criteria, final Map<String, Object> object) {
		return criteria;
	}

	/**
	 * Override this method to provide a find by example page criteria.
	 *
	 * @param criteria the criteria.
	 * @param object the sample object.
	 *
	 * @return the criteria.
	 */
	protected DetachedCriteria preparePageCriteria(final DetachedCriteria criteria, final Map<String, Object> object) {
		return criteria;
	}

	/**
	 * Override this method to provide a find by example list criteria.
	 *
	 * @param criteria the criteria.
	 * @param object the sample object.
	 *
	 * @return the criteria.
	 */
	protected DetachedCriteria prepareUniqueCriteria(final DetachedCriteria criteria, final T object) {
		return criteria;
	}

	/**
	 * Override this method to provide a find by example list criteria.
	 *
	 * @param criteria the criteria.
	 * @param object the sample object.
	 *
	 * @return the criteria.
	 */
	protected DetachedCriteria prepareUniqueCriteria(final DetachedCriteria criteria, final Map<String, Object> object) {
		return criteria;
	}

	/**
	 * Finds an object by its identifier.
	 *
	 * @param key the key.
	 *
	 * @return the object or null if not found.
	 */
	@SuppressWarnings("unchecked")
	@Transactional(readOnly = true, propagation = Propagation.SUPPORTS)
	public T findById(K key) {
		if(entityName != null) {
			return (T)getHibernateTemplate().get(entityName, (Serializable)key);
		} else {
			return (T)getHibernateTemplate().get(domainClass, (Serializable)key);
		}
	}

	/**
	 * Finds objects by its primary keys.
	 *
	 * @param keys the keys.
	 *
	 * @return the list of objects.
	 */
	@SuppressWarnings("unchecked")
	@Transactional(readOnly = true, propagation = Propagation.SUPPORTS)
	public List<T> findByIds(final K[] keys) {
		DetachedCriteria criteria = getDetachedCriteria();

		Session session = null;

		try {
			session = getSession();
			criteria.add(Restrictions.in(identifierName, keys));

			return criteria.getExecutableCriteria(session).list();
		} finally {
			releaseSession(session);
		}
	}

	/**
	 * Finds objects by its primary keys.
	 *
	 * @param keys the keys.
	 *
	 * @return the list of objects.
	 */
	@SuppressWarnings("unchecked")
	@Transactional(readOnly = true, propagation = Propagation.SUPPORTS)
	public List<T> findByIds(final Collection<K> keys) {
		DetachedCriteria criteria = getDetachedCriteria();

		Session session = null;

		try {
			session = getSession();
			criteria.add(Restrictions.in(identifierName, keys));

			return criteria.getExecutableCriteria(session).list();
		} finally {
			releaseSession(session);
		}
	}

	/**
	 * Retrieves the detached criteria.
	 *
	 * @return the detached criteria.
	 */
	protected DetachedCriteria getDetachedCriteria() {
		if(entityName != null) {
			return DetachedCriteria.forEntityName(entityName);
		} else {
			return DetachedCriteria.forClass(domainClass);
		}
	}

	/**
	 * Finds the objects that satisfy a search criteria expressed as an example object bean.
	 *
	 * @param sample the sample object bean.
	 *
	 * @return the results.
	 */
	@SuppressWarnings("unchecked")
	@Transactional(readOnly = true, propagation = Propagation.SUPPORTS)
	public List<T> findByExample(final T sample) {
		DetachedCriteria criteria = getDetachedCriteria();

		Session session = null;

		try {
			session = getSession();

			return prepareListCriteria(criteria, sample).getExecutableCriteria(session).list();
		} finally {
			releaseSession(session);
		}
	}

	/**
	 * Finds the objects that satisfy a search criteria expressed as an example object bean.
	 *
	 * @param sample the sample object bean.
	 *
	 * @return the results.
	 */
	@SuppressWarnings("unchecked")
	@Transactional(readOnly = true, propagation = Propagation.SUPPORTS)
	public List<T> findByExample(final Map<String, Object> sample) {
		DetachedCriteria criteria = getDetachedCriteria();

		Session session = null;

		try {
			session = getSession();

			return prepareListCriteria(criteria, sample).getExecutableCriteria(session).list();
		} finally {
			releaseSession(session);
		}
	}

	/**
	 * Finds the objects that satisfy a search criteria expressed as an example object bean and a pagination object
	 * to limit and sort.
	 *
	 * @param sample the sample criteria.
	 * @param start the start index.
	 * @param limit the limit index.
	 * @param sortFields the sort fields.
	 * @param sortDirections the sort directions.
	 *
	 * @return the page.
	 */
	@Transactional(readOnly = true, propagation = Propagation.SUPPORTS)
	public Page<T> findByExample(
		final Map<String, Object> sample, int start, int limit, final String[] sortFields, final String[] sortDirections) {
		DetachedCriteria criteria = getDetachedCriteria();

		// Prepare the page criteria
		criteria = preparePageCriteria(criteria, sample);


		// Build page and paginate
		PageImpl<T> page = buildPage();

		page.setStart(start);
		page.setLimit(limit);
		page.setCriteria(criteria);

		if((sortFields != null) && (sortDirections != null) && (sortFields.length == sortDirections.length)) {
			for(int i = 0; i < sortFields.length; ++i) {
				page.addSort(sortFields[i], sortDirections[i]);
			}
		}

		page.paginate();

		return page;
	}

	/**
	 * Builds a page. Override this to provide a custom behaviour.
	 *
	 * @return a page.
	 */
	public PageImpl<T> buildPage() {
		return new PageImpl<T>(getSessionFactory());
	}

	/**
	 * Finds the unique instance that satisfies a search criteria expressed as an example object bean.
	 *
	 * @param sample the sample object bean.
	 *
	 * @return the unique result.
	 */
	@SuppressWarnings("unchecked")
	@Transactional(readOnly = true, propagation = Propagation.SUPPORTS)
	public T findUniqueByExample(final T sample) {
		DetachedCriteria criteria = getDetachedCriteria();

		Session session = null;

		try {
			session = getSession();

			return (T)prepareUniqueCriteria(criteria, sample).getExecutableCriteria(session).uniqueResult();
		} finally {
			releaseSession(session);
		}
	}

	/**
	 * Finds the unique instance that satisfies a search criteria expressed as an example object bean.
	 *
	 * @param sample the sample object bean.
	 *
	 * @return the unique result.
	 */
	@SuppressWarnings("unchecked")
	@Transactional(readOnly = true, propagation = Propagation.SUPPORTS)
	public T findUniqueByExample(final Map<String, Object> sample) {
		DetachedCriteria criteria = getDetachedCriteria();

		Session session = null;

		try {
			session = getSession();

			return (T)prepareUniqueCriteria(criteria, sample).getExecutableCriteria(session).uniqueResult();
		} finally {
			releaseSession(session);
		}
	}

	/**
	 * Loads every single instance of a persistent class.
	 *
	 * @return list of instances.
	 */
	@SuppressWarnings("unchecked")
	@Transactional(readOnly = true, propagation = Propagation.SUPPORTS)
	public List<T> findAll() {
		DetachedCriteria criteria = getDetachedCriteria();

		Session session = null;

		try {
			session = getSession();

			return criteria.getExecutableCriteria(session).setResultTransformer(DetachedCriteria.DISTINCT_ROOT_ENTITY).list();
		} finally {
			releaseSession(session);
		}
	}

	/**
	 * Clears the cache.
	 */
	public void clearCache() {
		Session session = null;

		try {
			session = getSession();

			SessionFactory sessionFactory = session.getSessionFactory();

			if(entityName != null) {
				sessionFactory.evictEntity(entityName);
			} else {
				sessionFactory.evict(domainClass);
			}
		} finally {
			releaseSession(session);
		}
	}

	/**
	 * Clears the given caches.
	 *
	 * @param clazzes the classes.
	 */
	protected void clearCaches(final List<Class> clazzes) {
		Session session = null;

		try {
			session = getSession();

			SessionFactory sessionFactory = session.getSessionFactory();

			for(Class clazz : clazzes) {
				sessionFactory.evict(clazz);
			}
		} finally {
			releaseSession(session);
		}
	}

	/**
	 * Clears the given caches.
	 *
	 * @param entities the entities.
	 */
	protected void clearEntityCaches(final List<String> entities) {
		Session session = null;

		try {
			session = getSession();

			SessionFactory sessionFactory = session.getSessionFactory();

			for(String entityName : entities) {
				sessionFactory.evictEntity(entityName);
			}
		} finally {
			releaseSession(session);
		}
	}
}
